Skip to content

[WIP]: [Fix] Per-rank SHM + streaming 1× host peak + FX device placement - #64

Open
cennn wants to merge 56 commits into
mainfrom
fix/ep-offload-weight-corruption
Open

[WIP]: [Fix] Per-rank SHM + streaming 1× host peak + FX device placement#64
cennn wants to merge 56 commits into
mainfrom
fix/ep-offload-weight-corruption

Conversation

@cennn

@cennn cennn commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Motivation

model_cpu_offload materializes weights into /dev/shm so ranks on one node can share pinned pages. On main, rank 0 writes a single flat file per dtype and every rank maps it — correct when all ranks hold the same weights, but when ranks hold different shards (expert parallelism), every shard is silently replaced with rank 0's data. MoE routing still runs, experts are numerically wrong, producing garbled video.

The same main path allocates a full flat_buffer = torch.zeros(total_numel) before writing to disk, so host anonymous memory peaks at ~2× model size. For a 200 GB+ MoE model across 8 ranks this means ~700 GB peak — dangerously close to host OOM.

Separately, main only rewrites device= kwargs on factory functions (torch.empty, torch.zeros, …) in the FX graph. Offload tracing also emits .to(device("cpu")) call_method nodes and leaves CPU example_value metadata on arbitrary nodes, which Inductor cannot handle — it raises FakeTensor device mismatch on ops like aten.index_select receiving both cuda and cpu arguments.

Changes

1. Auto-detect per-rank SHM via weight fingerprint

Each rank computes a SHA256 fingerprint (param names + shapes + sampled head/tail data, < 1 s for any model size), then dist.all_gather compares across ranks. All identical → single shared mmap; any differ → per-rank mmap. Zero configuration needed for both DP/CP and EP scenarios.

Override: MAGI_COMPILE_OFFLOAD_CONFIG__FORCE_PER_RANK_WEIGHTS env var (1/0) bypasses auto-detection when needed.

2. Streaming copy-and-replace (1× host peak)

_stream_copy_and_replace copies each parameter into the mmap and immediately replaces it in the module, so only one parameter's worth of duplication exists at any moment. The old flat_buffer + tofile() + load_state_dict path is eliminated.

Path Anonymous growth vs model
main (flat_buffer) 260 MB 1.02×
this PR (streaming) 4 MB 0.02×

(measured via smaps_rollup Anonymous on 64 MB model in isolated subprocess)

3. Full FX device placement rewrite

fix_graph_device_placement (module-level pure function) rewrites:

  • call_function nodes with device=cpu kwargs (covers all ops, not just factory functions)
  • call_method .to(cpu) nodes (positional args and kwargs)
  • All CPU example_value metadata, recursively through nested list/tuple

Other

  • offload() recursively penetrates plain objects' __dict__ to move CUDA tensors to CPU, skipping nn.Module to avoid interfering with model internals.
  • _force_cpu skips the GPU round-trip when the tensor is already on CPU and fn does not change dtype.
  • host_memory.py: lightweight /proc-based host memory introspection (fmt_host_mem()), logged at key offload stages.
  • OffloadConfig.force_per_rank_weights: None (auto-detect) / True / False, exposed as Pydantic field with env var support via BaseSettings.

Evidence

  • 8×5090 base compile+offload warmup completes on the SHM path.
  • All-5090 E2E (textenc CPU + base/sr/vae 5090) produced valid videos from samples_5req_diverse.json.

[attachment: garbled vs fixed video comparison, base warmup logs — to be pasted]

Tests (+701 lines, 3 new files)

  • test_ep_shared_memory.py: 2-rank gloo repro — per_rank=False corrupts shards, per_rank=True preserves; 4 fingerprint auto-detection tests.
  • test_fix_to_cpu_in_graph.py: FX graph .to(cpu) + device=cpu rewrite — metadata-only fix fails, full rewrite succeeds.
  • test_shm_memory_peak.py: subprocess-isolated smaps_rollup Anonymous measurement — batch shows ~1× growth, streaming shows ~0× growth.

cennn added 4 commits August 27, 2026 18:59
_patch_cpu_offload_apply created a single shared-memory file from
local_rank=0 and had all ranks read it. With expert parallelism (EP>1),
each rank holds a different expert shard; reading rank-0 data on every
rank destroyed expert weight diversity and produced garbled video output.

Fix: when EP_SIZE>1, fall back to per-rank pin_memory instead of
cross-rank shared-memory dedup.

Also: move model weights to CUDA before Dynamo tracing (_deep_cuda) so
Dynamo captures the fused Triton kernel path instead of the decomposed
Python fallback, and extend _fix_graph_device_placement to fix ALL FX
nodes with CPU example_values (not just get_attr/placeholder).
…tion

Two tests using torch.multiprocessing.spawn with gloo backend:

1. test_shared_memory_overwrites_ep_shards:
   Reproduces the bug — local_rank=0 writes expert weights to a shared
   file, all other ranks read it, silently overwriting their own expert
   shards with rank 0's data.

2. test_ep_fix_preserves_per_rank_shards:
   Verifies the fix — when EP_SIZE > 1, the shared-memory path is
   skipped and each rank retains its own expert weights.
ENGINE_CONFIG__EP_SIZE may not be set if the host framework
(e.g. disagg_compute_runner) only sets EP_SIZE or configures
ep_size programmatically. Fall back to EP_SIZE env var before
defaulting to 1.
When EP_SIZE > 1, each rank holds a unique expert shard. The previous
fix skipped shared memory entirely and used pin_memory, which was
extremely slow for large models (~46GB per rank).

Now each rank writes its own shared-memory file to /dev/shm and
mmap-reads it back, preserving per-rank expert weights while keeping
the speed benefit of shared memory + pin_memory_in_place on
already-resident pages.

For EP_SIZE <= 1, the original rank-0-writes-all-read scheme is
retained (all ranks have identical weights).

Also updates the regression test to verify the per-rank shm path.
@cennn cennn changed the title fix(offload): skip shared-memory weight dedup when EP>1; move model to CUDA before tracing fix(offload): per-rank shared memory for EP>1 to prevent expert weight corruption Aug 27, 2026
cennn added 12 commits August 28, 2026 00:59
…_apply

With EP>1, all 8 ranks simultaneously created ~43GB flat_buffer + wrote ~43GB
to /dev/shm = ~87GB per rank x 8 = ~700GB, exceeding 512Gi container limit.

Fix: serialize writes across ranks (one at a time) and write directly into
mmap file (no flat_buffer). Peak memory drops from ~700GB to ~392GB.
1. _force_cpu: skip GPU roundtrip for CPU tensors when fn only changes
   device (not dtype). Reduces peak host memory during model.cuda() by
   avoiding temporary CUDA host allocations for every parameter.

2. MAGI_OFFLOAD_SKIP_SHM: when set to "1", skip shared memory creation
   and pin_memory_in_place entirely. Params remain as regular CPU tensors.
   This allows OffloadExecutor to work on memory-constrained nodes (e.g.
   5090 with 512Gi container limit for 8x EP ranks) where the shm+pin
   overhead causes OOM.
When converting CPU example_value metadata to CUDA for Inductor, the
.to(device) call strips torch.nn.Parameter wrapping. This caused
OffloadExecutor to misidentify all model weights as regular input
tensors, loading all 43.6GB onto GPU simultaneously instead of
offloading per-submodule — OOM on 5090 (31GB VRAM).

Re-wrap the converted FakeTensor in nn.Parameter to preserve type info.
- OffloadExecutor: log per-step H2D vs compute breakdown when MAGI_OFFLOAD_DEBUG=1
  (cuda.synchronize between prefetch and compute for accurate wall-clock split)
- _patch_cpu_offload_apply: support MAGI_OFFLOAD_PIN_BUDGET_GB env var
  Pin up to N GB of weights per rank via cudaHostRegister (no SHM copy)
  for faster async H2D while staying within host memory budget
Log start/end time for each rank during staggered pin, plus OS memlock
limit. Helps debug slow NFS page faults during cudaHostRegister.
Replace sequential 1-rank-at-a-time pinning with parallel waves.
Auto-detects max concurrent ranks: total_ram/2 / per_rank_param_size.
Override via MAGI_OFFLOAD_PIN_CONCURRENCY env var.

512GB node, 43.36GB/rank → concurrency=5, 2 waves instead of 8.
Expected pin time: ~6min vs ~22min sequential.
The per-submodule cuda.synchronize() barriers prevented H2D/compute
pipeline overlap, reducing production throughput. Profiling data has
been collected; this debug scaffolding is no longer needed.
- Revert offload_warpper.py (all changes were unused imports after debug removal)
- Remove dead offload() __dict__ branch (call sites only pass tuple/dict)
- Extract 80-line inline pin logic into _staggered_pin_memory() helper
- magi_backend.py: use module-level os/magi_logger instead of inline imports
- Use %-formatting instead of f-string for logger calls
…fload_apply

- _shm_path(): centralize /dev/shm path construction
- _pack_params_flat(): copy named tensors into contiguous buffer
- _split_flat_to_params(): split flat buffer back to named param views
- _create_shm_tensor(): create mmap file + pack in one call
- Unify EP>1 and EP<=1 serialization (both use mmap now, remove numpy bf16/fp8 workaround)
- _patch_cpu_offload_apply SHM logic: ~135 lines -> ~45 lines
- Merge per_rank_shm and shared branches into single helper
- Collapse 3-way dispatch to 2-way (skip_shm vs materialize)
- Single cleanup point for del/gc.collect()
- Eliminate duplicated pin/append/split/remove/load_state_dict
@cennn cennn added the ci:run Trigger CI integration tests label Aug 29, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Aug 29, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Aug 29, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Aug 29, 2026
…tracing

After _deep_cuda removal (e0c7277), Dynamo traces with CPU tensors and
specialises .to(x.device) as .to(device('cpu')) — a hardcoded literal in the
FX graph.  _fix_graph_device_placement already moves example_values to CUDA,
but these baked .to(cpu) nodes remained, causing index_select(CUDA, CPU) →
BackendCompilerFailed during PiecewiseCompileInterpreter.run().

Extend _fix_graph_device_placement to also rewrite:
  - call_method('to', device('cpu')) → call_method('to', device('cuda'))
  - call_function(..., device='cpu') → call_function(..., device='cuda')

Add regression test (test_fix_to_cpu_in_graph.py) that:
  1. Confirms metadata-only fix still produces the device mismatch
  2. Verifies the full rewrite resolves the error
  3. Ensures .to(dtype) calls are not affected
@cennn cennn added the ci:run Trigger CI integration tests label Aug 29, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Aug 29, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Aug 29, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Aug 29, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Aug 29, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Sep 1, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Sep 1, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Sep 1, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Sep 1, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Sep 1, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Sep 1, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
@cennn cennn added ci:run Trigger CI integration tests and removed ci:run Trigger CI integration tests labels Sep 1, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
@cennn cennn added the ci:run Trigger CI integration tests label Sep 1, 2026
@github-actions github-actions Bot removed the ci:run Trigger CI integration tests label Sep 1, 2026
…ARE_WEIGHTS

- Add _compute_weights_fingerprint(): SHA256 over param names + shapes +
  head/tail sampled data (~1 KB per param, < 1s for any model size)
- Add _all_ranks_same_weights(): all_gather fingerprints across ranks
- Auto-detect per_rank in _patch_cpu_offload_apply: if all ranks have
  identical weights -> shared mmap; otherwise per-rank mmap
- Rename env override: MAGI_COMPILE_OFFLOAD_CONFIG__FORCE_PER_RANK_WEIGHTS
  (replaces SHM_SHARE_WEIGHTS; None=auto, True=force per-rank, False=force share)
- Update OffloadConfig: shm_share_weights -> force_per_rank_weights (Optional[bool])
- Add 4 fingerprint unit tests (identical, different, deterministic, single-element)
@cennn cennn added the ci:run Trigger CI integration tests label Sep 1, 2026
…mpat)

Extract get_cpu_gloo_group() into utils/dist_utils.py, shared by
_all_ranks_same_weights and _get_cost_sync_group. Fixes RuntimeError
on NCCL-default process groups where CPU tensors are rejected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants